﻿unit ServerLogging;

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, System.SyncObjs,
  System.DateUtils, System.IOUtils, system.Types,
  Sparkle.Logger, EnhancedLogger;

type
  TLogLevel = (llDebug, llInfo, llWarning, llError, llFatal);

  IRequestLogger = interface
    ['{B3A8C7D9-4E5F-4A2B-8C1D-9E6F5A3B7C8E}']
    procedure LogRequest(const RequestID, ModuleName, Message: string;
      Level: TLogLevel = llInfo);
    procedure LogPerformance(const RequestID, Operation: string;
      Milliseconds: Int64);
    procedure Debug(const Msg: string);
    procedure Info(const Msg: string);
    procedure Warning(const Msg: string);
    procedure Error(const Msg: string);
    procedure Fatal(const Msg: string);
  end;

  // 日志记录项
  TLogItem = record
    RequestID: string;
    ModuleName: string;
    Message: string;
    Level: TLogLevel;
    Timestamp: TDateTime;
    IsPerformance: Boolean;
    Operation: string;
    Milliseconds: Int64;
  end;

  TFileLogEngine = class(TInterfacedObject, ILogEngine, IRequestLogger)
  private
    FBasePath: string;
    FMaxFileSize: Int64;
    FRetentionDays: Integer;
    FLock: TCriticalSection;
    FCurrentFileSizes: TDictionary<string, Int64>;
    FEnhancedLogger: IEnhancedLogEngine;

    // 异步日志相关
    FLogQueue: TQueue<TLogItem>;
    FQueueLock: TCriticalSection;
    FLogEvent: TEvent;
    FLogWorker: TThread;
    FShutdown: Boolean;

    function GetModuleLogPath(const ModuleName: string): string;
    function GetLogFileName(const ModuleName: string; const Date: TDateTime): string;
    procedure RotateLogFiles(const ModuleName: string);
    procedure CleanupOldFiles;
    procedure WriteToFile(const FileName, Message: string);
    procedure ProcessLogQueue; // 处理日志队列的方法
    procedure WriteLogItem(const LogItem: TLogItem); // 写入单个日志项
  public
    constructor Create(const BasePath: string; MaxFileSize: Int64 = 10 * 1024 * 1024;
      RetentionDays: Integer = 30);
    destructor Destroy; override;

    // ILogEngine
    procedure Log(const Msg: string); overload;

    // Enhanced logging methods
    procedure Debug(const Msg: string);
    procedure Info(const Msg: string);
    procedure Warning(const Msg: string);
    procedure Error(const Msg: string);
    procedure Fatal(const Msg: string);

    // IRequestLogger
    procedure LogRequest(const RequestID, ModuleName, Message: string;
      Level: TLogLevel = llInfo);
    procedure LogPerformance(const RequestID, Operation: string;
      Milliseconds: Int64);
  end;

  TRequestContext = class
  private
    FRequestID: string;
    FStartTime: TDateTime;
    FModuleName: string;
    FLogger: IRequestLogger;
  public
    constructor Create(const RequestID, ModuleName: string; Logger: IRequestLogger);
    destructor Destroy; override;

    procedure Debug(const Message: string);
    procedure Info(const Message: string);
    procedure Warning(const Message: string);
    procedure Error(const Message: string);
    procedure Fatal(const Message: string);
    procedure Log(const Message: string; Level: TLogLevel = llInfo);
    procedure LogPerformance(const Operation: string; Milliseconds: Int64);

    property RequestID: string read FRequestID;
    property ModuleName: string read FModuleName;
  end;

implementation

{ TFileLogEngine }

constructor TFileLogEngine.Create(const BasePath: string; MaxFileSize: Int64;
  RetentionDays: Integer);
begin
  inherited Create;
  FBasePath := IncludeTrailingPathDelimiter(BasePath);
  FMaxFileSize := MaxFileSize;
  FRetentionDays := RetentionDays;
  FLock := TCriticalSection.Create;
  FCurrentFileSizes := TDictionary<string, Int64>.Create;
  FEnhancedLogger := TEnhancedLogger.Create(Self as ILogEngine);

  // 初始化异步日志队列
  FLogQueue := TQueue<TLogItem>.Create;
  FQueueLock := TCriticalSection.Create;
  FLogEvent := TEvent.Create(nil, False, False, '');
  FShutdown := False;

  // 启动日志处理线程
  FLogWorker := TThread.CreateAnonymousThread(
    procedure
    begin
      ProcessLogQueue;
    end
  );
  FLogWorker.FreeOnTerminate := False;
  FLogWorker.Start;

  ForceDirectories(FBasePath);

  // 清理旧文件也在后台线程中进行
  TThread.CreateAnonymousThread(
    procedure
    begin
      CleanupOldFiles;
    end
  ).Start;
end;

destructor TFileLogEngine.Destroy;
begin
  // 标记关闭
  FShutdown := True;

  // 唤醒日志线程
  if Assigned(FLogEvent) then
    FLogEvent.SetEvent;

  // 等待工作线程结束
  if Assigned(FLogWorker) then
  begin
    FLogWorker.WaitFor;
    FLogWorker.Free;
  end;

  FLogQueue.Free;
  FQueueLock.Free;
  FLogEvent.Free;
  FCurrentFileSizes.Free;
  FLock.Free;
  inherited;
end;

procedure TFileLogEngine.ProcessLogQueue;
var
  LogItem: TLogItem;
  QueueEmpty: Boolean;
begin
  while not FShutdown do
  begin
    // 等待日志事件或超时
    FLogEvent.WaitFor(100); // 100ms 超时

    if FShutdown then Break;

    // 处理队列中的所有日志项
    while not FShutdown do
    begin
      FQueueLock.Enter;
      try
        QueueEmpty := FLogQueue.Count = 0;
        if not QueueEmpty then
          LogItem := FLogQueue.Dequeue;
      finally
        FQueueLock.Leave;
      end;

      if QueueEmpty then
        Break;

      // 写入日志项
      try
        WriteLogItem(LogItem);
      except
        on E: Exception do
        begin
          // 日志写入失败时的处理
          try
            TFile.AppendAllText(FBasePath + 'error.log',
              Format('[%s] Failed to write log: %s%s',
              [FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', Now), E.Message, sLineBreak]));
          except
            // 忽略错误日志写入失败
          end;
        end;
      end;
    end;
  end;

  // 关闭前处理剩余的所有日志项
  FQueueLock.Enter;
  try
    while FLogQueue.Count > 0 do
    begin
      LogItem := FLogQueue.Dequeue;
      try
        WriteLogItem(LogItem);
      except
        // 忽略关闭时的错误
      end;
    end;
  finally
    FQueueLock.Leave;
  end;
end;

procedure TFileLogEngine.WriteLogItem(const LogItem: TLogItem);
const
  LevelNames: array[TLogLevel] of string = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL');
var
  LogMsg, FileName: string;
  CurrentSize: Int64;
begin
  if LogItem.IsPerformance then
  begin
    LogMsg := Format('[%s] [%s] [ReqID:%s] Performance - %s: %d ms',
      [FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', LogItem.Timestamp),
       LevelNames[llInfo], LogItem.RequestID, LogItem.Operation, LogItem.Milliseconds]);
    FileName := GetLogFileName('Performance', LogItem.Timestamp);
  end
  else
  begin
    LogMsg := Format('[%s] [%s] [ReqID:%s] %s',
      [FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', LogItem.Timestamp),
       LevelNames[LogItem.Level], LogItem.RequestID, LogItem.Message]);
    FileName := GetLogFileName(LogItem.ModuleName, LogItem.Timestamp);
  end;

  // 检查文件大小
  FLock.Enter;
  try
    if not FCurrentFileSizes.TryGetValue(FileName, CurrentSize) then
    begin
      if TFile.Exists(FileName) then
        CurrentSize := TFile.GetSize(FileName)
      else
        CurrentSize := 0;
    end;

    // 如果文件过大，创建带时间戳的新文件
    if CurrentSize + Length(LogMsg) > FMaxFileSize then
    begin
      FileName := GetLogFileName(LogItem.ModuleName, LogItem.Timestamp) + '.' +
                 FormatDateTime('hhnnsszzz', LogItem.Timestamp) + '.log';
      CurrentSize := 0;
    end;

    // 写入日志
    WriteToFile(FileName, LogMsg);
  finally
    FLock.Leave;
  end;
end;

function TFileLogEngine.GetModuleLogPath(const ModuleName: string): string;
begin
  Result := FBasePath + ModuleName + PathDelim;
  if not TDirectory.Exists(Result) then
    ForceDirectories(Result);
end;

function TFileLogEngine.GetLogFileName(const ModuleName: string;
  const Date: TDateTime): string;
begin
  Result := GetModuleLogPath(ModuleName) +
            FormatDateTime('yyyy-mm-dd', Date) + '.log';
end;

procedure TFileLogEngine.RotateLogFiles(const ModuleName: string);
var
  LogPath: string;
  Files: TStringDynArray;
  I: Integer;
  FileDate: TDateTime;
  FileName: string;
  FormatSettings: TFormatSettings;
begin
  LogPath := GetModuleLogPath(ModuleName);
  if not TDirectory.Exists(LogPath) then Exit;

  FormatSettings := TFormatSettings.Create;
  FormatSettings.DateSeparator := '-';
  FormatSettings.ShortDateFormat := 'yyyy/mm/dd';

  Files := TDirectory.GetFiles(LogPath, '*.log');
  for I := 0 to High(Files) do
  begin
    FileName := ExtractFileName(Files[I]);
    if Length(FileName) >= 10 then
    begin
      if TryStrToDate(Copy(FileName, 1, 10), FileDate, FormatSettings) then
      begin
        if DaysBetween(Now, FileDate) > FRetentionDays then
        begin
          try
            TFile.Delete(Files[I]);
            FLock.Enter;
            try
              FCurrentFileSizes.Remove(Files[I]);
            finally
              FLock.Leave;
            end;
          except
            on E: Exception do
            begin
              // 异步记录删除失败
              Log('Failed to delete old log file: ' + Files[I] + ' - ' + E.Message);
            end;
          end;
        end;
      end;
    end;
  end;
end;

procedure TFileLogEngine.CleanupOldFiles;
var
  Dirs: TStringDynArray;
  I: Integer;
begin
  if not TDirectory.Exists(FBasePath) then Exit;

  Dirs := TDirectory.GetDirectories(FBasePath);
  for I := 0 to High(Dirs) do
    RotateLogFiles(ExtractFileName(Dirs[I]));

  RotateLogFiles('Global');
end;

procedure TFileLogEngine.WriteToFile(const FileName, Message: string);
var
  FileStream: TFileStream;
  Bytes: TBytes;
  CurrentSize: Int64;
begin
  try
    if TFile.Exists(FileName) then
      FileStream := TFileStream.Create(FileName, fmOpenWrite or fmShareDenyWrite)
    else
      FileStream := TFileStream.Create(FileName, fmCreate or fmShareDenyWrite);

    try
      FileStream.Seek(0, soEnd);
      Bytes := TEncoding.UTF8.GetBytes(Message + sLineBreak);
      FileStream.Write(Bytes[0], Length(Bytes));

      // 更新文件大小跟踪
      CurrentSize := FileStream.Position;
      FLock.Enter;
      try
        FCurrentFileSizes.AddOrSetValue(FileName, CurrentSize);
      finally
        FLock.Leave;
      end;
    finally
      FileStream.Free;
    end;
  except
    on E: Exception do
    begin
      // 如果文件写入失败，尝试写入错误日志
      try
        TFile.AppendAllText(FBasePath + 'error.log',
          Format('[%s] Failed to write log: %s - %s%s',
          [FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', Now), FileName, E.Message, sLineBreak]));
      except
        // 如果错误日志也失败，则忽略
      end;
    end;
  end;
end;

procedure TFileLogEngine.Log(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llInfo);
end;

procedure TFileLogEngine.Debug(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llDebug);
end;

procedure TFileLogEngine.Info(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llInfo);
end;

procedure TFileLogEngine.Warning(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llWarning);
end;

procedure TFileLogEngine.Error(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llError);
end;

procedure TFileLogEngine.Fatal(const Msg: string);
begin
  LogRequest('SYSTEM', 'Global', Msg, llFatal);
end;

procedure TFileLogEngine.LogRequest(const RequestID, ModuleName, Message: string;
  Level: TLogLevel);
var
  LogItem: TLogItem;
begin
  if FShutdown then Exit; // 如果正在关闭，不再接受新日志

  LogItem.RequestID := RequestID;
  LogItem.ModuleName := ModuleName;
  LogItem.Message := Message;
  LogItem.Level := Level;
  LogItem.Timestamp := Now;
  LogItem.IsPerformance := False;
  LogItem.Operation := '';
  LogItem.Milliseconds := 0;

  // 异步添加到队列
  FQueueLock.Enter;
  try
    FLogQueue.Enqueue(LogItem);
  finally
    FQueueLock.Leave;
  end;

  // 唤醒日志线程
  FLogEvent.SetEvent;
end;

procedure TFileLogEngine.LogPerformance(const RequestID, Operation: string;
  Milliseconds: Int64);
var
  LogItem: TLogItem;
begin
  if FShutdown then Exit;

  LogItem.RequestID := RequestID;
  LogItem.ModuleName := 'Performance';
  LogItem.Message := '';
  LogItem.Level := llInfo;
  LogItem.Timestamp := Now;
  LogItem.IsPerformance := True;
  LogItem.Operation := Operation;
  LogItem.Milliseconds := Milliseconds;

  // 异步添加到队列
  FQueueLock.Enter;
  try
    FLogQueue.Enqueue(LogItem);
  finally
    FQueueLock.Leave;
  end;

  // 唤醒日志线程
  FLogEvent.SetEvent;
end;

{ TRequestContext }

constructor TRequestContext.Create(const RequestID, ModuleName: string;
  Logger: IRequestLogger);
begin
  inherited Create;
  FRequestID := RequestID;
  FModuleName := ModuleName;
  FLogger := Logger;
  FStartTime := Now;

  Logger.Info(Format('Request started - Module: %s', [ModuleName]));
end;

destructor TRequestContext.Destroy;
var
  TotalTime: Int64;
begin
  TotalTime := MilliSecondsBetween(Now, FStartTime);
  FLogger.Info(Format('Request completed - Total time: %d ms', [TotalTime]));
  FLogger.LogPerformance(FRequestID, 'TotalRequest', TotalTime);
  inherited;
end;

procedure TRequestContext.Debug(const Message: string);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, llDebug);
end;

procedure TRequestContext.Info(const Message: string);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, llInfo);
end;

procedure TRequestContext.Warning(const Message: string);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, llWarning);
end;

procedure TRequestContext.Error(const Message: string);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, llError);
end;

procedure TRequestContext.Fatal(const Message: string);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, llFatal);
end;

procedure TRequestContext.Log(const Message: string; Level: TLogLevel);
begin
  FLogger.LogRequest(FRequestID, FModuleName, Message, Level);
end;

procedure TRequestContext.LogPerformance(const Operation: string;
  Milliseconds: Int64);
begin
  FLogger.LogPerformance(FRequestID, Operation, Milliseconds);
end;

end.
